
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.Box;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;

public final class ParticleApplet extends JApplet implements MouseListener, ActionListener {

    public final static int size = 500;
    private final static int n = 5; // Initial number of particles
    private final CopyOnWriteArrayList<Particle> particles = new CopyOnWriteArrayList<Particle>();
    private final CopyOnWriteArrayList<Thread> threads = new CopyOnWriteArrayList<Thread>();
    private final Random rng = new Random();
    private final JMenuBar menuBar = new JMenuBar();
    private final JButton startButton = new JButton("Start ");
    private final JMenuItem sizeButton = new JMenuItem("Particle Size (" + Particle.size + ")");
    private final JMenuItem frictionButton = new JMenuItem("Force of Friction (" + Particle.friction + ")");
    private final JMenuItem waveAmplitudeButton = new JMenuItem("Wave Amplitude (" + Particle.waveAmplitude + ")");
    private final JMenuItem wallBounceButton = new JMenuItem("Wall Bounce (" + Particle.wallBounce + ")");
    private final JMenuItem speedLimitButton = new JMenuItem("Speed Limit (" + Particle.speedLimit + ")");
    private final JMenuItem defaultsButton = new JMenuItem("Restore defaults");
    private final JMenu editMenu = new JMenu("Edit");
    private final JLabel count = new JLabel("Particles: " + particles.size());
    private boolean started = false;
    private final JMenu submenu = new JMenu();
    private final Canvas canvas = new Canvas() {

        @Override
        public void paint(Graphics g) {
            for (Iterator i = particles.iterator(); i.hasNext();)
                ((Particle) i.next()).draw(g);
            getToolkit().sync();
        }
    };

    @Override
    public void init() {
        defaults();
        canvas.setSize(new Dimension(size, size));
        canvas.addMouseListener(this);
        startButton.addMouseListener(this);
        sizeButton.addActionListener(this);
        frictionButton.addActionListener(this);
        waveAmplitudeButton.addActionListener(this);
        wallBounceButton.addActionListener(this);
        speedLimitButton.addActionListener(this);
        defaultsButton.addActionListener(this);
        editMenu.add(sizeButton);
        editMenu.add(frictionButton);
        editMenu.add(waveAmplitudeButton);
        editMenu.add(wallBounceButton);
        editMenu.add(speedLimitButton);
        editMenu.add(defaultsButton);
        menuBar.add(editMenu);
        menuBar.add(Box.createHorizontalGlue());
        menuBar.add(count);
        menuBar.add(Box.createHorizontalGlue());
        menuBar.add(startButton);
        setJMenuBar(menuBar);
        add(canvas);
    }

    @Override
    public synchronized void start() {
        for (int i = 0; i < n; i++)
            makeParticle(rng.nextInt(size), rng.nextInt(size));
    }

    synchronized protected void makeParticle(int x, int y) {
        if (particles.size() >= 65) return;
        final Particle p1 = new Particle(x, y, 2 * Math.pow(-1, rng.nextInt(2)), 2 * Math.pow(-1, rng.nextInt(2)));
        Thread collideThread = null;
        for (Iterator i = particles.iterator(); i.hasNext();) {
            final Particle p2 = (Particle) i.next();
            collideThread = new Thread(new Runnable() {

                public void run() {
                    try {
                        for (;;) {
                            Rectangle p1s = p1.shape.get();
                            Rectangle p2s = p2.shape.get();
                            if (p1s.intersects(p2s)) {
                                Vector v1 = p1.velocity.get();
                                p1.velocity.set(p2.velocity.get());
                                p2.velocity.set(v1);
                                p1.hit();
                                p2.hit();
                                canvas.repaint();
                                do {
                                    Thread.sleep(40);
                                    p1s = p1.shape.get();
                                    p2s = p2.shape.get();
                                } while (p1s.intersects(p2s));
                            } else Thread.sleep(25);
                        }
                    } catch (InterruptedException e) {
                        return;
                    }
                }
            });
            threads.add(collideThread);
            if (started) collideThread.start();
        }
        Thread moveThread = new Thread(new Runnable() {

            public void run() {
                try {
                    for (;;) {
                        if (p1.updateVelocity()) {
                            p1.move();
                            canvas.repaint();
                        }
                        Thread.sleep(85);
                    }
                } catch (InterruptedException e) {
                    return;
                }
            }
        });
        threads.add(moveThread);
        particles.add(p1);
        if (started) moveThread.start();
        count.setText("Particles: " + particles.size());
    }

    @Override
    public synchronized void stop() {
        for (Iterator i = threads.iterator(); i.hasNext();) {
            Thread t = (Thread) i.next();
            t.interrupt();
            threads.remove(t);
        }
        for (Iterator i = particles.iterator(); i.hasNext();)
            particles.remove((Particle) i.next());
    }

    public synchronized void startThreads() {
        for (Iterator i = threads.iterator(); i.hasNext();) {
            Thread t = (Thread) i.next();
            if (!t.isAlive()) t.start();
        }
    }

    public void mouseClicked(MouseEvent e) {
        if (e.getSource().equals(startButton))
            if (!started) {
                startThreads();
                startButton.setText("Reset");
                started = true;
            } else {
                stop();
                started = false;
                start();
                startButton.setText("Start ");
                canvas.repaint();
            }
        else {
            Point point = e.getPoint();
            makeParticle(point.x, point.y);
            canvas.repaint();
        }
    }

    public void actionPerformed(ActionEvent e) {
        if (e.getSource().equals(sizeButton)) {
            String s = JOptionPane.showInputDialog(canvas, "Input a positive integer for the size of the particles:", Particle.size);
            boolean success = false;
            int value = Particle.size;
            if ((s != null) && (s.length() > 0))
                do try {
                        value = Integer.parseInt(s);
                        if (value < 1) throw (new Exception());
                        else success = true;
                    } catch (Exception ex) {
                        s = JOptionPane.showInputDialog(canvas, "Invalid input. Input a positive integer for the size of the particles:", Particle.size);
                    }
                while (!success);
            updateSize(value);
        } else if (e.getSource().equals(frictionButton)) {
            String s = JOptionPane.showInputDialog(canvas, "Input a non-negative value for the force of friction:", Particle.friction);
            boolean success = false;
            double value = Particle.friction;
            if ((s != null) && (s.length() > 0))
                do try {
                        value = Double.parseDouble(s);
                        if (value < 0) throw (new Exception());
                        else success = true;
                    } catch (Exception ex) {
                        s = JOptionPane.showInputDialog(canvas, "Invalid input. Input a non-negative value for the force of friction:", Particle.friction);
                    }
                while (!success);
            updateFriction(value);
        } else if (e.getSource().equals(waveAmplitudeButton)) {
            String s = JOptionPane.showInputDialog(canvas, "Input a value for the wave amplitude:", Particle.waveAmplitude);
            boolean success = false;
            double value = Particle.waveAmplitude;
            if ((s != null) && (s.length() > 0))
                do try {
                        value = Double.parseDouble(s);
                        success = true;
                    } catch (Exception ex) {
                        s = JOptionPane.showInputDialog(canvas, "Invalid input. Input a value for the wave amplitude:", Particle.waveAmplitude);
                    }
                while (!success);
            updateWaveAmplitude(value);
        } else if (e.getSource().equals(wallBounceButton)) {
            String s = JOptionPane.showInputDialog(canvas, "Input a non-negative value for the wall bounce:", Particle.wallBounce);
            boolean success = false;
            double value = Particle.wallBounce;
            if ((s != null) && (s.length() > 0))
                do try {
                        value = Double.parseDouble(s);
                        if (value < 0) throw (new Exception());
                        else success = true;
                    } catch (Exception ex) {
                        s = JOptionPane.showInputDialog(canvas, "Invalid input. Input a non-negative value for the wall bounce:", Particle.wallBounce);
                    }
                while (!success);
            updateWallBounce(value);
        } else if (e.getSource().equals(speedLimitButton)) {
            String s = JOptionPane.showInputDialog(canvas, "Input a non-negative value for the speed limit, or 0 for none:", Particle.speedLimit);
            boolean success = false;
            double value = Particle.speedLimit;
            if ((s != null) && (s.length() > 0))
                do try {
                        if ("infinity".contains(s) || "INFINITY".contains(s)) s = "0";
                        value = Double.parseDouble(s);
                        if (value < 0) throw (new Exception());
                        else success = true;
                    } catch (Exception ex) {
                        s = JOptionPane.showInputDialog(canvas, "Invalid input. Input a non-negative value for the speed limit, or 0 for none:", Particle.speedLimit);
                    }
                while (!success);
            updateSpeedLimit(value);
        } else if (e.getSource().equals(defaultsButton)) defaults();
    }

    void defaults() {
        updateSize(Particle.defaultSize);
        updateFriction(Particle.defaultFriction);
        updateWaveAmplitude(Particle.defaultWaveAmplitude);
        updateWallBounce(Particle.defaultWallBounce);
        updateSpeedLimit(Particle.defaultSpeedLimit);
    }

    void updateSize(int value) {
        if (Particle.size != value && value > 0) {
            Particle.size = value;
            if (!started)
                synchronized (this) {
                    for (Iterator i = particles.iterator(); i.hasNext();)
                        ((Particle) i.next()).shape.get().setSize(Particle.size, Particle.size);
                }
            sizeButton.setText("Particle Size (" + Particle.size + ")");
            canvas.repaint();
        }
    }

    void updateFriction(double value) {
        if (Particle.friction != value && value >= 0) {
            Particle.friction = value;
            frictionButton.setText("Force of Friction (" + Particle.friction + ")");
            canvas.repaint();
        }
    }

    void updateWaveAmplitude(double value) {
        if (Particle.waveAmplitude != value) {
            Particle.waveAmplitude = value;
            waveAmplitudeButton.setText("Wave Amplitude (" + Particle.waveAmplitude + ")");
            canvas.repaint();
        }
    }

    void updateWallBounce(double value) {
        if (Particle.wallBounce != value && value >= 0) {
            Particle.wallBounce = value;
            wallBounceButton.setText("Wall Bounce (" + Particle.wallBounce + ")");
            canvas.repaint();
        }
    }

    void updateSpeedLimit(double value) {
        if (Particle.speedLimit != value && value >= 0) {
            Particle.speedLimit = (value > 0) ? value : Double.POSITIVE_INFINITY;
            speedLimitButton.setText("Speed Limit (" + Particle.speedLimit + ")");
            canvas.repaint();
        }
    }

    public void mousePressed(MouseEvent e) {
    }

    public void mouseReleased(MouseEvent e) {
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }
}
